﻿using System;
using System.Collections.Generic;
using System.Text;
using log4net.Core;
using System.Xml.Serialization;
using System.Threading;
using System.IO;
using System.Collections.Specialized;
using UPNP.IO;
using RouterControlVersion=RouterControl.IO.RouterControlVersion;

namespace RouterControl.Whs.ErrorReporting
{
    /// <summary>
    /// handles error reporting.
    /// 
    /// there is a limit how many errors may be enqueued in the memory.
    /// additional entries will be droppped!
    /// 
    /// ORDER:
    /// Add() -> enqueue (_unsavedErrors) -> filter (_filterThread) ->
    /// map (_filteredErrors) -> persist (_persistThread) -- file system --> 
    /// submit (_submitThread)
    /// </summary>
    public sealed class ErrorReportingSingleton
    {
        public const string ErrorReportingBaseDirPath = @"D:\folders\{3EB3E1B1-5A8B-45E3-9F9A-573D1088B3B5}\Errors\"; // keep in sync with RouterControl.WixSetup/Product.wxs/ErrorReportingDir
        public const int MaxEnqueueCount = 1000;
        public const int PersistedErrorHashCacheSize = 100;

        public delegate void OnErrorSubmittedDelegate(ErrorInfo error, int index, int totalErrorCount);

        #region error info

        [XmlRoot(ElementName="err", Namespace="http://www.fkollmann.de/RouterControl/ErrorReporting")]
        public class ErrorInfo
        {
            [XmlAttribute("v")]
            public int RouterControlVersionId = RouterControlVersion.VersionId;

            [XmlAttribute("ts")]
            public DateTime TimeStamp = DateTime.Now;

            [XmlAttribute("c")]
            public int Count = 1;

            [XmlElement("type")]
            public string ErrorType;

            [XmlElement("msg")]
            public string Message;

            [XmlElement("class")]
            public string ClassName;

            [XmlElement("func")]
            public string MethodName;

            [XmlElement("trace")]
            public string StackTrace;

            [XmlElement("inner")]
            public ErrorInfo Inner;

            [XmlAttribute("hash")]
            public string ErrorHash
            {
                get
                {
                    return DeviceHelpers.CreateId(
                        ClassName,
                        MethodName,
                        ErrorType
                        );
                }
            }

            [XmlIgnore]
            public string PersitedErrorFilename
            {
                get { return (this.ErrorHash + ".xml"); }
            }

            public Web.ErrorInfo ToWebService()
            {
                return new Web.ErrorInfo()
                {
                    InstanceGuid = RouterControlRegistry.GetInstanceId().ToString(),

                    VersionId = this.RouterControlVersionId,
                    TimeStampUtc = this.TimeStamp.ToUniversalTime(),
                    ErrorHash = this.ErrorHash,
                    Count = this.Count,
                    ErrorType = this.ErrorType,

                    Message = this.Message,

                    ClassName = this.ClassName,
                    MethodName = this.MethodName,
                    StackTrace = this.StackTrace,

                    Inner = (this.Inner == null ? null : this.Inner.ToWebService())
                };
            }
        }

        #endregion

        private static readonly ErrorReportingSingleton _instance = new ErrorReportingSingleton();
        
        public static ErrorReportingSingleton Instance
        {
            get { return _instance; }
        }

        public event OnErrorSubmittedDelegate OnErrorSubmitted;

        private readonly log4net.ILog _log = log4net.LogManager.GetLogger("ErrorReporting");    // keep in sync with log4net configs!
        private readonly DirectoryInfo _baseDir = new DirectoryInfo(ErrorReportingBaseDirPath);
        private Queue<ErrorInfo> _unsavedErrors = new Queue<ErrorInfo>();
        private Thread _filterThread;
        private Queue<ErrorInfo> _filteredErrors = new Queue<ErrorInfo>();
        private StringCollection _persistedErrorHashCache = new StringCollection();
        private Thread _persistThread;
        private Thread _submitThread;
        private volatile bool _stopAllThreads = false;
        private volatile bool _stopSubmittingErrors = false;
        private readonly XmlSerializer _errorInfoXs = new XmlSerializer(typeof(ErrorInfo));

        public int GetUnsubmittedErrorCount()
        {
            return _baseDir.GetFiles("*.xml").Length;
        }

        private ErrorReportingSingleton()
        {
            // create error reporting directory
            if (!_baseDir.Exists)
                _baseDir.Create();

            // install exception handling
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
            AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit);

            // start filtering
            _filterThread = new Thread(new ThreadStart(ThreadStartFilterErrors))
            {
                IsBackground = true,
                Priority = ThreadPriority.Normal
            };

            _filterThread.Start();

            // start persisting
            _persistThread = new Thread(new ThreadStart(ThreadStartPersistErrors))
            {
                IsBackground = true,
                Priority = ThreadPriority.BelowNormal
            };

            _persistThread.Start();
        }

        public void StartSubmittingErrors()
        {
            if (_stopAllThreads)
                throw new InvalidOperationException("all threads have been stopped; restart prohibited");

            if ((_submitThread == null) || !_submitThread.IsAlive)
            {
                _stopSubmittingErrors = false;

                _submitThread = new Thread(new ThreadStart(ThreadStartSubmitErrors))
                {
                    IsBackground = true,
                    Priority = ThreadPriority.Lowest
                };

                _submitThread.Start();
            }
        }

        public void StopSubmittingErrors(bool waitForStop)
        {
            if (_stopAllThreads)
                return;

            if ((_submitThread != null) && _submitThread.IsAlive)
            {
                _stopSubmittingErrors = true;

                if (waitForStop)
                    _submitThread.Join();
            }
        }

        private void ThreadStartFilterErrors()
        {
            if (_log.IsDebugEnabled)
                _log.Debug("Start filtering errors...");

            while (!_stopAllThreads)
            {
                FilterErrors();

                if (_stopAllThreads)
                    break;

                // wait for new errors to come
                var secs = Properties.Settings.Default.ErrorReportingFilterIntervalSeconds;

                for (int i = 0; (i < 2 * secs) && !_stopAllThreads; i++)
                    System.Threading.Thread.Sleep(500);
            }
        }

        private void FilterErrors()
        {
            while (_unsavedErrors.Count > 0)
            {
                // terminate?
                if (_stopAllThreads)
                    break;

                // continue
                var ei = _unsavedErrors.Dequeue();

                try
                {
                    // drop error which already have been persisted
                    if (_persistedErrorHashCache.Contains(ei.ErrorHash))
                        continue;

                    // error is now filtered
                    _filteredErrors.Enqueue(ei);
                }
                catch (Exception x)
                {
                    _log.Error("Failed to filter error info " + ei.ErrorHash, x);
                }
            }
        }

        private void ThreadStartPersistErrors()
        {
            if (_log.IsDebugEnabled)
                _log.Debug("Start persisting errors...");

            while (!_stopAllThreads)
            {
                PersistErrors();

                if (_stopAllThreads)
                    break;

                // wait for new errors to come
                var secs = Properties.Settings.Default.ErrorReportingPersistIntervalSeconds;

                for (int i = 0; (i < 2 * secs) && !_stopAllThreads; i++)
                    System.Threading.Thread.Sleep(500);
            }
        }

        private void PersistErrors()
        {
            while (_filteredErrors.Count > 0)
            {
                // terminate?
                if (_stopAllThreads)
                    break;

                // continue
                var ei = _filteredErrors.Dequeue();

                try
                {
                    // add to filter
                    _persistedErrorHashCache.Add(ei.ErrorHash);

                    // does the file exist?
                    var errorFile = new FileInfo(
                        Path.Combine(_baseDir.FullName, ei.PersitedErrorFilename)
                        );

                    // drop error which have already been persisted
                    if (errorFile.Exists)
                        continue;

                    // persist error
                    using (var tw = errorFile.CreateText())
                    {
                        _errorInfoXs.Serialize(tw, ei);
                    }

                    if (_log.IsDebugEnabled)
                        _log.Debug("Error persisted: " + errorFile.Name);

                    // cleanup cache
                    while (_persistedErrorHashCache.Count > PersistedErrorHashCacheSize)
                        _persistedErrorHashCache.RemoveAt(0);

                }
                catch (Exception x)
                {
                    _log.Error("Failed to persist error info: " + ei.ErrorHash, x);
                }
            }
        }

        private void ThreadStartSubmitErrors()
        {
            if (_log.IsDebugEnabled)
                _log.Debug("Start submitting errors...");

            while (!_stopAllThreads && !_stopSubmittingErrors)
            {
                SubmitErrors();

                if (_stopAllThreads || _stopSubmittingErrors)
                    break;

                // wait for new errors to come
                var secs = Properties.Settings.Default.ErrorReportingSubmitIntervalSeconds;

                for (int i = 0; (i < 2 * secs) && !_stopAllThreads && !_stopSubmittingErrors; i++)
                    System.Threading.Thread.Sleep(500);
            }
        }

        internal void SubmitErrors()
        {
            var found = _baseDir.GetFiles("*.xml");
            var counter = 0;

            // prepare
            if (found.Length <= 0)
                return;

            if (_log.IsDebugEnabled)
                _log.Debug("Submitting " + found.Length + " errors...");

            if (this.OnErrorSubmitted != null)
                this.OnErrorSubmitted(null, counter, found.Length);

            // perform submitting
            foreach (var errorFile in found)
            {
                // terminate?
                if (_stopAllThreads || _stopSubmittingErrors)
                    break;

                // continue
                try
                {
                    // try to deserialize file
                    ErrorInfo ei;

                    using (var tr = errorFile.OpenText())
                    {
                        ei = (ErrorInfo)_errorInfoXs.Deserialize(tr);
                    }

                    // submit error info then
                    using (var ws = new Web.RouterControlWS2Factory())
                    {
                        ws.Channel.ReportError(ei.ToWebService());
                    }

                    // finalize
                    errorFile.Delete();

                    _log.Info("Submitted error: " + errorFile.Name);

                    if (this.OnErrorSubmitted != null)
                        this.OnErrorSubmitted(ei, ++counter, found.Length);

                }
                catch (Exception x)
                {
                    _log.Error("Failed to submit error info " + errorFile.Name, x);
                }
            }
        }

        private void CurrentDomain_ProcessExit(object sender, EventArgs e)
        {
            _log.Info("Process is exiting");

            // persists known errors first
            StopAllThreads(false);      // we only have three seconds; don't wait

            FilterErrors();
            PersistErrors();
        }

        private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            var x = (Exception)e.ExceptionObject;

            _log.Fatal("Unhandled exception", x);

            AddException(x);

            // if terminating, persist all errors first!
            if (e.IsTerminating)
            {
                StopAllThreads(true);

                FilterErrors();
                PersistErrors();
            }
        }

        private void StopAllThreads(bool wait)
        {
            if (_log.IsDebugEnabled)
                _log.Debug("Stopping all threads...");

            _stopAllThreads = true;

            try
            {
                if (wait)
                {
                    _filterThread.Join(10000);
                    _persistThread.Join(1);

                    _filterThread.Abort();
                    _persistThread.Abort();
                }

                _log.Info("All threads stopped; wait: " + wait);
            }
            catch (Exception x)
            {
                _log.Error("Failed to wait for threads to stop", x);
            }
        }

        public void AddException(Exception e)
        {
            if (e == null)
                return;

            // queue if full?
            if (_unsavedErrors.Count >= MaxEnqueueCount)
            {
                _log.Warn("Queue is full (max = " + MaxEnqueueCount + ")");

                return;
            }

            // enqueue
            var ei = new ErrorInfo()
            {
                RouterControlVersionId = RouterControlVersion.VersionId,
            };

            ParseException(ei, e);

            _unsavedErrors.Enqueue(ei);
        }

        public void AddLoggingEvent(LoggingEvent e)
        {
            if (e == null)
                return;

            // queue if full?
            if (_unsavedErrors.Count >= MaxEnqueueCount)
            {
                _log.Warn("Queue is full (max = " + MaxEnqueueCount + ")");

                return;
            }

            // enqueue
            var ei = new ErrorInfo()
            {
                RouterControlVersionId = RouterControlVersion.VersionId,
                TimeStamp = e.TimeStamp,
                Message = e.RenderedMessage,
                ErrorType = typeof(LoggingEvent).Name
            };

            if (e.LocationInformation != null)
            {
                ei.ClassName = e.LocationInformation.ClassName;
                ei.MethodName = e.LocationInformation.MethodName;
            }

            if (e.ExceptionObject != null)
            {
                var inner = new ErrorInfo();

                ParseException(inner, e.ExceptionObject);

                ei.Inner = inner;
            }

            _unsavedErrors.Enqueue(ei);
        }

        private void ParseException(ErrorInfo ei, Exception x)
        {
            if (ei == null)
                throw new ArgumentNullException("ei");

            if (x == null)
                return;

            // copy data
            ei.ErrorType = x.GetType().Name;
            ei.Message = x.Message;
            ei.StackTrace = x.StackTrace;
            ei.MethodName = x.TargetSite.Name;
            ei.ClassName = x.TargetSite.DeclaringType.FullName;

            // recurse
            if (x.InnerException != null)
            {
                var inner = new ErrorInfo();

                ParseException(inner, x.InnerException);

                ei.Inner = inner;
            }
        }
    }
}
